/*
 * Copyright (C) 2010-2011 Canonical Ltd <jeremy.kerr@canonical.com>
 * Copyright (C) 2011-2012 Linaro Ltd <mturquette@linaro.org>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation.
 *
 * Standard functionality for the common clock API.  See Documentation/driver-api/clk.rst
 */

#include <linux/clk.h>
#include <linux/clk-provider.h>
#include <linux/clk/clk-conf.h>
#include <linux/module.h>
#include <linux/mutex.h>
#include <linux/spinlock.h>
#include <linux/err.h>
#include <linux/list.h>
#include <linux/slab.h>
#include <linux/of.h>
#include <linux/device.h>
#include <linux/init.h>
#include <linux/pm_runtime.h>
#include <linux/sched.h>
#include <linux/clkdev.h>

/***    private data structures    ***/

struct clk_core {
	const char		*name;
	const struct clk_ops	*ops;
	struct clk_hw		*hw;
	struct module		*owner;
	struct device		*dev;
	struct clk_core		*parent;
	const char		**parent_names;
	struct clk_core		**parents;
	u8			num_parents;
	u8			new_parent_index;
	unsigned long		rate;
	unsigned long		req_rate;
	unsigned long		new_rate;
	struct clk_core		*new_parent;
	struct clk_core		*new_child;
	unsigned long		flags;
	bool			orphan;
	unsigned int		enable_count;
	unsigned int		prepare_count;
	unsigned int		protect_count;
	unsigned long		min_rate;
	unsigned long		max_rate;
	unsigned long		accuracy;
	int			phase;
	struct clk_duty		duty;
	struct hlist_head	children;
	struct hlist_node	child_node;
	struct hlist_head	clks;
	unsigned int		notifier_count;
#ifdef CONFIG_DEBUG_FS
	struct dentry		*dentry;
	struct hlist_node	debug_node;
#endif
	struct kref		ref;
};

struct clk {
	struct clk_core	*core;
	const char *dev_id;
	const char *con_id;
	unsigned long min_rate;
	unsigned long max_rate;
	unsigned int exclusive_count;
	struct hlist_node clks_node;
};

#if defined(CONFIG_LOMBO_CLK)
#include "linux/clk/lombo_clk.h"
#include "csp/lombo_clk_csp.h"
#include "clk.h"
#include <mach/debug.h>

/* start bit index and width for mode field */
#define PLL_MODE_SHIFT		0
#define PLL_MODE_WIDTH		2

int judge_pll_name(struct clk_hw *hw, int mode)
{
	const char *name;
	int i, val;

	val = 0;
	name = clk_hw_get_name(hw);

	if (mode == PLL_MODE_LINEAR) {
		for (i = 0; i < pll_linear_counts; i++) {
			if (!strcmp(name, pll_linear_name[i]))
				val |= 1;
		}
	} else if (mode == PLL_MODE_SPREAD) {
		for (i = 0; i < pll_spread_counts; i++) {
			if (!strcmp(name, pll_spread_name[i]))
				val |= 1;
		}
	}

	if (val == 0)
		return -EPERM;
	else
		return 0;
}

int clk_pll_enable_spread_during_linear(struct clk *clk, u32 enable)
{
	struct clk_hw *hw = clk->core->hw;
	struct clock_pll *pll = to_clock_pll(hw);
	unsigned long flags = 0;
	int mode, ret;

	ret = judge_pll_name(hw, PLL_MODE_LINEAR);
	if (ret) {
		PRT_ERR("this clk is not support linear mode\n");
		goto out;
	}

	spin_lock_irqsave(&pll->lock, flags);
	mode = readl(pll->reg[MODE]);
	if (mode == PLL_MODE_LINEAR) {
		csp_enable_spread_during_linear(enable, pll->reg[TUNE2]);
		ret = 0;
	} else {
		ret = -EPERM;
		PRT_ERR("pll mode is not linear now\n");
	}
	spin_unlock_irqrestore(&pll->lock, flags);

out:
	return ret;
}
EXPORT_SYMBOL_GPL(clk_pll_enable_spread_during_linear);

int clk_pll_set_linear_adjust_freq_step(struct clk *clk, u32 step)
{
	struct clk_hw *hw = clk->core->hw;
	struct clock_pll *pll = to_clock_pll(hw);
	unsigned long flags = 0;
	int mode, ret;

	ret = judge_pll_name(hw, PLL_MODE_LINEAR);
	if (ret) {
		PRT_ERR("this clk is not support linear mode\n");
		goto out;
	}

	spin_lock_irqsave(&pll->lock, flags);
	mode = readl(pll->reg[MODE]);
	if (mode == PLL_MODE_LINEAR) {
		csp_set_linear_adjust_freq_step(step, pll->reg[TUNE2]);
		ret = 0;
	} else {
		ret = -EPERM;
		PRT_ERR("pll mode is not linear now\n");
	}
	spin_unlock_irqrestore(&pll->lock, flags);

out:
	return ret;
}
EXPORT_SYMBOL_GPL(clk_pll_set_linear_adjust_freq_step);

int clk_pll_set_spread_para(struct clk *clk, u32 amplitude, u32 period)
{
	struct clk_hw *hw = clk->core->hw;
	struct clock_pll *pll = to_clock_pll(hw);
	unsigned long flags = 0;
	int mode, ret;

	ret = judge_pll_name(hw, PLL_MODE_SPREAD);
	if (ret) {
		PRT_ERR("this clk is not support spread mode\n");
		goto out;
	}

	spin_lock_irqsave(&pll->lock, flags);
	mode = readl(pll->reg[MODE]);
	if (mode == PLL_MODE_SPREAD) {
		csp_set_spread_para(amplitude, period, pll->reg[TUNE1]);
		ret = 0;
	} else {
		ret = -EPERM;
		PRT_ERR("pll mode is not spread now\n");
	}
	spin_unlock_irqrestore(&pll->lock, flags);

out:
	return ret;
}
EXPORT_SYMBOL_GPL(clk_pll_set_spread_para);

void clk_pll_set_mode(struct clk *clk, int mode)
{
	struct clk_hw *hw = clk->core->hw;
	struct clock_pll *pll = to_clock_pll(hw);
	unsigned long flags = 0;
	u32 val;

#if defined(CONFIG_ARCH_LOMBO_N7V3) || defined(CONFIG_ARCH_LOMBO_N7V5) \
	|| defined(CONFIG_ARCH_LOMBO_N5V1) || defined(CONFIG_ARCH_LOMBO_N9V3)
	const char *name = clk_hw_get_name(hw);
	reg_prcm_audio_adpll_fractune_t reg;

	if (!strcmp(name, "audio_pll")) {
		spin_lock_irqsave(&pll->lock, flags);
		WARN_ON(mode < 0 || mode >= 2);

		reg.val = readl(pll->reg[NFAC]);
		reg.bits.ssc_frac_en = mode;
		writel(reg.val, pll->reg[NFAC]);
		spin_unlock_irqrestore(&pll->lock, flags);

		return;
	}
#endif

	spin_lock_irqsave(&pll->lock, flags);
	WARN_ON(mode < 0 || mode >= PLL_MODE_MAX);

	val = readl(pll->reg[MODE]);
	val &= ~(((1 << PLL_MODE_WIDTH) - 1) << PLL_MODE_SHIFT);
	val |= (mode << PLL_MODE_SHIFT);
	writel(val, pll->reg[MODE]);

	spin_unlock_irqrestore(&pll->lock, flags);
}
EXPORT_SYMBOL_GPL(clk_pll_set_mode);

int clk_pll_get_mode(struct clk *clk)
{
	struct clk_hw *hw = clk->core->hw;
	struct clock_pll *pll = to_clock_pll(hw);
	unsigned long flags = 0;
	int val, mode = 0;

	spin_lock_irqsave(&pll->lock, flags);
	val = readl(pll->reg[MODE]);
	mode = (val >> PLL_MODE_SHIFT) & ((1 << PLL_MODE_WIDTH) - 1);
	spin_unlock_irqrestore(&pll->lock, flags);

	WARN_ON(mode < 0 || mode >= PLL_MODE_MAX);
	return mode;
}
EXPORT_SYMBOL_GPL(clk_pll_get_mode);
#else
#error "Not support!"
#endif